package com.icongyou.enterprise.talent_analysis.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.icongyou.enterprise.common.exception.BusinessException;
import com.icongyou.enterprise.talent_analysis.entity.DimensionDetail;
import com.icongyou.enterprise.talent_analysis.entity.PortraitResponse;
import com.icongyou.enterprise.talent_analysis.entity.PortraitTemplate;
import com.icongyou.enterprise.talent_analysis.entity.dto.StudentFeatureContext;
import com.icongyou.enterprise.talent_analysis.entity.template.TemplateDimension;
import com.icongyou.enterprise.talent_analysis.entity.template.TemplateMetricMapping;
import com.icongyou.enterprise.talent_analysis.mapper.PortraitTemplateMapper;
import com.icongyou.enterprise.talent_analysis.service.impl.AIPortraitEvaluationServiceImpl;
import com.icongyou.enterprise.talent_analysis.service.impl.StudentFeatureExtractor;
import com.icongyou.enterprise.talent_analysis.service.support.AIClient;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;

import java.util.*;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.ArgumentMatchers.*;
import static org.mockito.Mockito.*;

/**
 * AI画像评估服务测试
 */
@ExtendWith(MockitoExtension.class)
@DisplayName("AI画像评估服务测试")
class AIPortraitEvaluationServiceImplTest {

    @Mock
    private StudentFeatureExtractor studentFeatureExtractor;

    @Mock
    private PortraitTemplateMapper portraitTemplateMapper;

    @Mock
    private AIClient aiClient;

    @Mock
    private PortraitService portraitService;
    
    @InjectMocks
    private AIPortraitEvaluationServiceImpl service;
    
    @Spy
    private ObjectMapper objectMapper = new ObjectMapper(); // 使用Spy创建真实ObjectMapper

    private static final String STUDENT_ID = "660e8400-e29b-41d4-a716-446655440105";
    private static final String TEMPLATE_ID = "template-1";

    private PortraitTemplate template;
    private StudentFeatureContext features;

    @BeforeEach
    void setUp() throws Exception {
        // 注入真实的ObjectMapper (因为@InjectMocks不会注入@Spy字段)
        java.lang.reflect.Field objectMapperField = AIPortraitEvaluationServiceImpl.class.getDeclaredField("objectMapper");
        objectMapperField.setAccessible(true);
        objectMapperField.set(service, objectMapper);
        
        template = createTemplate();
        features = createFeatures();
    }

    // ==================== computeCustomPortrait 测试 ====================

    @Test
    @DisplayName("computeCustomPortrait - 模板不存在抛出异常")
    void testComputeCustomPortrait_TemplateNotFound() {
        // Arrange
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(null);

        // Act & Assert
        BusinessException exception = assertThrows(BusinessException.class, () ->
            service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID)
        );
        assertEquals("模板不存在", exception.getMessage());
    }

    @Test
    @DisplayName("computeCustomPortrait - 维度不明确时降级到默认画像")
    void testComputeCustomPortrait_UnclearDimensions_FallbackToDefault() throws Exception {
        // Arrange - 创建不明确的维度（A, B, C等）
        String dimensionsJson = "[{\"name\":\"A\",\"code\":\"A\",\"weight\":50},{\"name\":\"B\",\"code\":\"B\",\"weight\":50}]";
        template.setDimensionsJson(dimensionsJson);
        
        PortraitResponse defaultResponse = new PortraitResponse();
        defaultResponse.getLabels().addAll(Arrays.asList("技术能力", "问题解决", "学习能力"));
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(portraitService.computeDefaultPortrait(STUDENT_ID)).thenReturn(defaultResponse);

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertNotNull(response);
        assertTrue(response.isDimensionsUnclear());
        assertTrue(response.isFallbackToDefault());
        assertNotNull(response.getDimensionWarningMessage());
        verify(portraitService).computeDefaultPortrait(STUDENT_ID);
    }

    @Test
    @DisplayName("computeCustomPortrait - 使用已有评分标准")
    void testComputeCustomPortrait_WithExistingStandards() throws Exception {
        // Arrange - 使用真实的JSON
        String dimensionsJson = objectMapper.writeValueAsString(createDimensionsWithMetrics());
        template.setDimensionsJson(dimensionsJson);
        template.setAiSuggested(true); // 已有AI生成的标准
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(features);

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertNotNull(response);
        assertTrue(response.isAiUsed());
        assertEquals(2, response.getLabels().size());
        assertEquals(2, response.getScores().size());
        verify(aiClient, never()).generateScoringStandard(anyString()); // 不应调用AI
    }

    @Test
    @DisplayName("computeCustomPortrait - 首次使用生成评分标准")
    void testComputeCustomPortrait_FirstTimeGenerateStandards() throws Exception {
        // Arrange
        String dimensionsJson = objectMapper.writeValueAsString(createDimensionsWithoutMetrics());
        template.setDimensionsJson(dimensionsJson);
        template.setAiSuggested(false); // 首次使用
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(features);
        when(aiClient.evaluateDimension(anyString())).thenReturn("4.2"); // Mock AI评分
        lenient().when(portraitTemplateMapper.updateById(any())).thenReturn(1); // 使用lenient因为可能不会更新

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertNotNull(response);
        assertTrue(response.isAiUsed());
        assertEquals(2, response.getLabels().size());
        assertEquals(2, response.getScores().size());
    }

    @Test
    @DisplayName("computeCustomPortrait - 计算所有维度得分")
    void testComputeCustomPortrait_CalculateAllScores() throws Exception {
        // Arrange
        List<TemplateDimension> dims = createDimensionsWithMetrics();
        String dimensionsJson = objectMapper.writeValueAsString(dims);
        template.setDimensionsJson(dimensionsJson);
        template.setAiSuggested(true);
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(features);

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertNotNull(response);
        assertEquals(2, response.getLabels().size());
        assertEquals(2, response.getScores().size());
        for (Double score : response.getScores()) {
            assertTrue(score >= 0 && score <= 5, "得分应在0-5之间: " + score);
        }
    }

    // ==================== extractStudentFeatures 测试 ====================

    @Test
    @DisplayName("extractStudentFeatures - 正常提取")
    void testExtractStudentFeatures() {
        // Arrange
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(features);

        // Act
        StudentFeatureContext result = service.extractStudentFeatures(STUDENT_ID);

        // Assert
        assertNotNull(result);
        assertEquals(STUDENT_ID, result.getStudentId());
        verify(studentFeatureExtractor).extractFeatures(STUDENT_ID);
    }

    // ==================== generateScoringStandards 测试 ====================

    @Test
    @DisplayName("generateScoringStandards - 为所有维度生成标准")
    void testGenerateScoringStandards_AllDimensions() throws Exception {
        // Arrange
        List<TemplateDimension> dims = createDimensionsWithoutMetrics();
        List<StudentFeatureContext> samples = Collections.singletonList(features);
        String aiResponse = createAIScoringResponse();
        
        when(aiClient.generateScoringStandard(anyString())).thenReturn(aiResponse);

        // Act
        List<TemplateDimension> result = service.generateScoringStandards(dims, samples);

        // Assert
        assertNotNull(result);
        for (TemplateDimension dim : result) {
            assertNotNull(dim.getMetrics(), "维度 " + dim.getName() + " 应该有指标");
            assertFalse(dim.getMetrics().isEmpty(), "维度 " + dim.getName() + " 的指标不应为空");
        }
        verify(aiClient, times(dims.size())).generateScoringStandard(anyString());
    }

    @Test
    @DisplayName("generateScoringStandards - 跳过已有指标的维度")
    void testGenerateScoringStandards_SkipExisting() throws Exception {
        // Arrange
        List<TemplateDimension> dims = new ArrayList<>();
        TemplateDimension withMetrics = new TemplateDimension();
        withMetrics.setName("已有指标");
        withMetrics.setMetrics(Collections.singletonList(new TemplateMetricMapping()));
        dims.add(withMetrics);
        
        TemplateDimension withoutMetrics = new TemplateDimension();
        withoutMetrics.setName("无指标");
        dims.add(withoutMetrics);
        
        String aiResponse = createAIScoringResponse();
        when(aiClient.generateScoringStandard(anyString())).thenReturn(aiResponse);

        // Act
        List<TemplateDimension> result = service.generateScoringStandards(dims, Collections.singletonList(features));

        // Assert
        assertNotNull(result);
        verify(aiClient, times(1)).generateScoringStandard(anyString()); // 只为无指标的维度调用
    }

    @Test
    @DisplayName("generateScoringStandards - 权重归一化")
    void testGenerateScoringStandards_WeightNormalization() throws Exception {
        // Arrange
        List<TemplateDimension> dims = createDimensionsWithoutMetrics();
        String aiResponse = "```json\n{\"metrics\":[{\"metricKey\":\"avg_score\",\"weight\":0.6},{\"metricKey\":\"pass_rate\",\"weight\":0.5}]}\n```";
        
        when(aiClient.generateScoringStandard(anyString())).thenReturn(aiResponse);

        // Act
        List<TemplateDimension> result = service.generateScoringStandards(dims, Collections.singletonList(features));

        // Assert
        assertNotNull(result);
        for (TemplateDimension dim : result) {
            if (dim.getMetrics() != null && !dim.getMetrics().isEmpty()) {
                double totalWeight = dim.getMetrics().stream()
                    .mapToDouble(TemplateMetricMapping::getWeight)
                    .sum();
                assertEquals(1.0, totalWeight, 0.01, "权重总和应为1.0");
            }
        }
    }

    // ==================== recommendDataMapping 测试 ====================

    @Test
    @DisplayName("recommendDataMapping - 推荐数据映射")
    void testRecommendDataMapping() throws Exception {
        // Arrange
        String aiResponse = "[\"avg_course_score\", \"avg_assignment_score\", \"assignment_pass_rate\"]";
        when(aiClient.recommendMapping(anyString())).thenReturn(aiResponse);

        // Act
        List<String> result = service.recommendDataMapping("技术能力", "评估学生的编程和技术能力");

        // Assert
        assertNotNull(result);
        assertEquals(3, result.size());
        assertTrue(result.contains("avg_course_score"));
    }

    // ==================== Helper Methods ====================

    private PortraitTemplate createTemplate() {
        PortraitTemplate tpl = new PortraitTemplate();
        tpl.setId(TEMPLATE_ID);
        tpl.setTemplateName("测试模板");
        tpl.setEnterpriseId("enterprise-1");
        tpl.setAiSuggested(false);
        return tpl;
    }

    private StudentFeatureContext createFeatures() {
        StudentFeatureContext ctx = new StudentFeatureContext();
        ctx.setStudentId(STUDENT_ID);
        ctx.setStudentName("孙七");
        ctx.setMajor("计算机科学与技术");
        
        Map<String, Object> metrics = new HashMap<>();
        metrics.put("avg_course_score", 85.0);
        metrics.put("avg_assignment_score", 90.0);
        metrics.put("assignment_pass_rate", 0.9);
        metrics.put("contribution_rate_avg", 25.0);
        metrics.put("avg_team_size", 4.0);
        ctx.setNumericMetrics(metrics);
        
        // 添加统计摘要数据(防止NPE)
        StudentFeatureContext.StatisticalSummary summary = new StudentFeatureContext.StatisticalSummary();
        summary.setTotalCourses(5);
        summary.setTotalAssignments(20);
        summary.setCompletedAssignments(18);
        summary.setOnTimeRate(0.85);
        summary.setPassRate(0.9);
        ctx.setSummary(summary);
        
        return ctx;
    }

    private List<TemplateDimension> createDimensionsWithMetrics() {
        List<TemplateDimension> dims = new ArrayList<>();
        
        TemplateDimension dim1 = new TemplateDimension();
        dim1.setName("技术能力");
        dim1.setCode("TECH");
        dim1.setWeight(50.0);
        
        TemplateMetricMapping metric1 = new TemplateMetricMapping();
        metric1.setMetricKey("avg_course_score");
        metric1.setWeight(0.6);
        
        TemplateMetricMapping metric2 = new TemplateMetricMapping();
        metric2.setMetricKey("avg_assignment_score");
        metric2.setWeight(0.4);
        
        dim1.setMetrics(Arrays.asList(metric1, metric2));
        dims.add(dim1);
        
        TemplateDimension dim2 = new TemplateDimension();
        dim2.setName("问题解决");
        dim2.setCode("PROBLEM");
        dim2.setWeight(50.0);
        
        TemplateMetricMapping metric3 = new TemplateMetricMapping();
        metric3.setMetricKey("assignment_pass_rate");
        metric3.setWeight(1.0);
        
        dim2.setMetrics(Collections.singletonList(metric3));
        dims.add(dim2);
        
        return dims;
    }

    private List<TemplateDimension> createDimensionsWithoutMetrics() {
        List<TemplateDimension> dims = new ArrayList<>();
        
        TemplateDimension dim1 = new TemplateDimension();
        dim1.setName("技术能力");
        dim1.setCode("TECH");
        dims.add(dim1);
        
        TemplateDimension dim2 = new TemplateDimension();
        dim2.setName("学习能力");
        dim2.setCode("LEARNING");
        dims.add(dim2);
        
        return dims;
    }

    private String createAIScoringResponse() {
        // 返回符合parseAIScoringResponse预期格式的JSON
        return "```json\n" +
               "{\n" +
               "  \"metrics\": [\n" +
               "    {\"metricKey\": \"avg_course_score\", \"weight\": 0.6},\n" +
               "    {\"metricKey\": \"avg_assignment_score\", \"weight\": 0.4}\n" +
               "  ],\n" +
               "  \"textAnalysisRules\": \"关键词: ['编程', '代码', '技术']; 正面: ['优秀', '出色']; 负面: ['需改进']\",\n" +
               "  \"scoringLogic\": \"基础分=数值指标加权平均; 文本调整=关键词×0.1+正面×0.3-负面×0.3\"\n" +
               "}\n" +
               "```";
    }

    // ==================== extractTeacherCommentKeywords 测试 ====================

    @Test
    @DisplayName("extractTeacherCommentKeywords - 成功提取关键词")
    void testExtractTeacherCommentKeywords_Success() throws Exception {
        // Arrange
        StudentFeatureContext ctx = createFeatures();
        StudentFeatureContext.TeacherComment comment1 = new StudentFeatureContext.TeacherComment();
        comment1.setAssignmentName("作业1");
        comment1.setCommentText("代码质量很好，逻辑清晰");
        
        StudentFeatureContext.TeacherComment comment2 = new StudentFeatureContext.TeacherComment();
        comment2.setAssignmentName("作业2");
        comment2.setCommentText("团队协作能力强");
        
        ctx.setTeacherComments(Arrays.asList(comment1, comment2));
        
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(ctx);
        when(aiClient.extractKeywords(anyList())).thenReturn("{\"代码质量\": 3, \"团队协作\": 2}");

        // Act
        Map<String, Integer> result = service.extractTeacherCommentKeywords(STUDENT_ID);

        // Assert
        assertNotNull(result);
        assertTrue(result.containsKey("代码质量"));
        assertEquals(3, result.get("代码质量"));
        verify(aiClient).extractKeywords(anyList());
    }

    @Test
    @DisplayName("extractTeacherCommentKeywords - 无评语返回空Map")
    void testExtractTeacherCommentKeywords_NoComments() {
        // Arrange
        StudentFeatureContext ctx = createFeatures();
        ctx.setTeacherComments(new ArrayList<>());
        
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(ctx);

        // Act
        Map<String, Integer> result = service.extractTeacherCommentKeywords(STUDENT_ID);

        // Assert
        assertNotNull(result);
        assertTrue(result.isEmpty());
        verify(aiClient, never()).extractKeywords(anyList());
    }

    @Test
    @DisplayName("extractTeacherCommentKeywords - AI解析失败返回空Map")
    void testExtractTeacherCommentKeywords_ParseFailure() {
        // Arrange
        StudentFeatureContext ctx = createFeatures();
        StudentFeatureContext.TeacherComment comment = new StudentFeatureContext.TeacherComment();
        comment.setCommentText("评语");
        ctx.setTeacherComments(Collections.singletonList(comment));
        
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(ctx);
        when(aiClient.extractKeywords(anyList())).thenReturn("invalid json");

        // Act
        Map<String, Integer> result = service.extractTeacherCommentKeywords(STUDENT_ID);

        // Assert
        assertNotNull(result);
        assertTrue(result.isEmpty());
    }

    // ==================== computeCustomPortrait 边界情况测试 ====================

    @Test
    @DisplayName("computeCustomPortrait - 权重不为1.0时自动归一化")
    void testComputeCustomPortrait_AutoNormalizeWeights() throws Exception {
        // Arrange
        List<TemplateDimension> dims = createDimensionsWithMetrics();
        // 修改权重使其总和不为1.0
        dims.get(0).getMetrics().get(0).setWeight(0.8);
        dims.get(0).getMetrics().get(1).setWeight(0.5); // 总和1.3
        
        String dimensionsJson = objectMapper.writeValueAsString(dims);
        template.setDimensionsJson(dimensionsJson);
        template.setAiSuggested(true);
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(features);

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertNotNull(response);
        // 应该能正常计算，不会因为权重问题失败
        assertTrue(response.getScores().size() > 0);
    }

    @Test
    @DisplayName("computeCustomPortrait - 维度名为连续字母序列时降级")
    void testComputeCustomPortrait_ConsecutiveLetters_Fallback() throws Exception {
        // Arrange
        String dimensionsJson = "[{\"name\":\"ABC\",\"code\":\"ABC\",\"weight\":50},{\"name\":\"DEF\",\"code\":\"DEF\",\"weight\":50}]";
        template.setDimensionsJson(dimensionsJson);
        
        PortraitResponse defaultResponse = new PortraitResponse();
        defaultResponse.getLabels().add("默认维度");
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(portraitService.computeDefaultPortrait(STUDENT_ID)).thenReturn(defaultResponse);

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertTrue(response.isDimensionsUnclear());
        assertTrue(response.isFallbackToDefault());
        verify(portraitService).computeDefaultPortrait(STUDENT_ID);
    }

    @Test
    @DisplayName("computeCustomPortrait - 维度名只有数字时降级")
    void testComputeCustomPortrait_NumericNames_Fallback() throws Exception {
        // Arrange
        String dimensionsJson = "[{\"name\":\"123\",\"code\":\"123\",\"weight\":50},{\"name\":\"456\",\"code\":\"456\",\"weight\":50}]";
        template.setDimensionsJson(dimensionsJson);
        
        PortraitResponse defaultResponse = new PortraitResponse();
        defaultResponse.getLabels().add("默认维度");
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(portraitService.computeDefaultPortrait(STUDENT_ID)).thenReturn(defaultResponse);

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertTrue(response.isDimensionsUnclear());
        verify(portraitService).computeDefaultPortrait(STUDENT_ID);
    }

    @Test
    @DisplayName("computeCustomPortrait - 有效维度名不降级")
    void testComputeCustomPortrait_ValidNames_NoFallback() throws Exception {
        // Arrange
        List<TemplateDimension> dims = createDimensionsWithMetrics();
        String dimensionsJson = objectMapper.writeValueAsString(dims);
        template.setDimensionsJson(dimensionsJson);
        template.setAiSuggested(true);
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(features);

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertFalse(response.isDimensionsUnclear());
        assertFalse(response.isFallbackToDefault());
        verify(portraitService, never()).computeDefaultPortrait(anyString());
    }

    @Test
    @DisplayName("recommendDataMapping - 空描述时使用默认提示")
    void testRecommendDataMapping_EmptyDescription() throws Exception {
        // Arrange
        when(aiClient.recommendMapping(anyString())).thenReturn("[]");

        // Act
        List<String> result = service.recommendDataMapping("技术能力", "");

        // Then
        assertNotNull(result);
        verify(aiClient).recommendMapping(contains("仅提供了维度名称"));
    }

    @Test
    @DisplayName("recommendDataMapping - null描述时使用默认提示")
    void testRecommendDataMapping_NullDescription() throws Exception {
        // Arrange
        when(aiClient.recommendMapping(anyString())).thenReturn("[]");

        // Act
        List<String> result = service.recommendDataMapping("技术能力", null);

        // Then
        assertNotNull(result);
        verify(aiClient).recommendMapping(contains("仅提供了维度名称"));
    }

    // ==================== 文本分析测试 ====================

    @Test
    @DisplayName("computeCustomPortrait - 文本分析规则(本地关键词匹配)")
    void testComputeCustomPortrait_TextAnalysisLocal() throws Exception {
        // Arrange
        List<TemplateDimension> dims = new ArrayList<>();
        TemplateDimension dim = new TemplateDimension();
        dim.setName("技术能力");
        dim.setCode("TECH");
        dim.setWeight(100.0);
        
        // 设置数值指标
        TemplateMetricMapping metric = new TemplateMetricMapping();
        metric.setMetricKey("avg_assignment_score");
        metric.setWeight(1.0);
        dim.setMetrics(Collections.singletonList(metric));
        
        // 设置文本分析规则(JSON格式)
        Map<String, Object> textRules = new HashMap<>();
        textRules.put("keywords", Arrays.asList("代码", "算法", "编程"));
        textRules.put("positiveIndicators", Arrays.asList("优秀", "出色"));
        textRules.put("negativeIndicators", Arrays.asList("需改进"));
        textRules.put("bonusPoints", Arrays.asList("教师推荐"));
        dim.setTextAnalysisRules(objectMapper.writeValueAsString(textRules));
        
        dims.add(dim);
        
        String dimensionsJson = objectMapper.writeValueAsString(dims);
        template.setDimensionsJson(dimensionsJson);
        template.setAiSuggested(true);
        
        // 添加教师评语
        StudentFeatureContext ctx = createFeatures();
        StudentFeatureContext.TeacherComment comment = new StudentFeatureContext.TeacherComment();
        comment.setAssignmentName("作业1");
        comment.setCommentText("代码质量优秀，算法实现出色，强烈教师推荐");
        ctx.setTeacherComments(Collections.singletonList(comment));
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(ctx);

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertNotNull(response);
        assertEquals(1, response.getScores().size());
        // 分数应该被文本分析调整(关键词2个+正面2个+加分1个)
        Double score = response.getScores().get(0);
        assertTrue(score > 0, "分数应该大于0: " + score);
        // 不应该调用AI(本地分析成功)
        verify(aiClient, never()).evaluateDimension(anyString());
    }

    @Test
    @DisplayName("computeCustomPortrait - 文本分析失败回退到AI")
    void testComputeCustomPortrait_TextAnalysisFallbackToAI() throws Exception {
        // Arrange
        List<TemplateDimension> dims = new ArrayList<>();
        TemplateDimension dim = new TemplateDimension();
        dim.setName("技术能力");
        dim.setCode("TECH");
        
        TemplateMetricMapping metric = new TemplateMetricMapping();
        metric.setMetricKey("avg_assignment_score");
        metric.setWeight(1.0);
        dim.setMetrics(Collections.singletonList(metric));
        
        // 设置无效的文本分析规则(触发解析失败)
        dim.setTextAnalysisRules("invalid json format");
        dims.add(dim);
        
        String dimensionsJson = objectMapper.writeValueAsString(dims);
        template.setDimensionsJson(dimensionsJson);
        template.setAiSuggested(true);
        
        StudentFeatureContext ctx = createFeatures();
        StudentFeatureContext.TeacherComment comment = new StudentFeatureContext.TeacherComment();
        comment.setCommentText("很好的作品");
        ctx.setTeacherComments(Collections.singletonList(comment));
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(ctx);
        when(aiClient.evaluateDimension(anyString())).thenReturn("0.5");

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertNotNull(response);
        // 应该回退到AI分析
        verify(aiClient).evaluateDimension(anyString());
    }

    @Test
    @DisplayName("computeCustomPortrait - 无教师评语时跳过文本分析")
    void testComputeCustomPortrait_NoComments_SkipTextAnalysis() throws Exception {
        // Arrange
        List<TemplateDimension> dims = createDimensionsWithMetrics();
        dims.get(0).setTextAnalysisRules("{\"keywords\":[\"测试\"]}");
        
        String dimensionsJson = objectMapper.writeValueAsString(dims);
        template.setDimensionsJson(dimensionsJson);
        template.setAiSuggested(true);
        
        StudentFeatureContext ctx = createFeatures();
        ctx.setTeacherComments(new ArrayList<>()); // 无评语
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(ctx);

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertNotNull(response);
        // 不应调用AI(因为没有文本数据)
        verify(aiClient, never()).evaluateDimension(anyString());
        verify(aiClient, never()).analyzeText(anyString(), anyString());
    }

    @Test
    @DisplayName("computeCustomPortrait - 负面评价减分")
    void testComputeCustomPortrait_NegativeComments() throws Exception {
        // Arrange
        List<TemplateDimension> dims = new ArrayList<>();
        TemplateDimension dim = new TemplateDimension();
        dim.setName("代码质量");
        
        TemplateMetricMapping metric = new TemplateMetricMapping();
        metric.setMetricKey("avg_assignment_score");
        metric.setWeight(1.0);
        dim.setMetrics(Collections.singletonList(metric));
        
        Map<String, Object> textRules = new HashMap<>();
        textRules.put("keywords", Arrays.asList("代码"));
        textRules.put("negativeIndicators", Arrays.asList("不规范", "需改进", "有问题"));
        dim.setTextAnalysisRules(objectMapper.writeValueAsString(textRules));
        dims.add(dim);
        
        String dimensionsJson = objectMapper.writeValueAsString(dims);
        template.setDimensionsJson(dimensionsJson);
        template.setAiSuggested(true);
        
        StudentFeatureContext ctx = createFeatures();
        StudentFeatureContext.TeacherComment comment = new StudentFeatureContext.TeacherComment();
        comment.setCommentText("代码不规范，需改进，存在有问题的地方");
        ctx.setTeacherComments(Collections.singletonList(comment));
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(ctx);

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertNotNull(response);
        // 分数应该被负面评价降低
        Double score = response.getScores().get(0);
        assertNotNull(score);
    }

    // ==================== 维度评估测试 ====================

    @Test
    @DisplayName("computeCustomPortrait - 维度无指标时使用AI整体评估")
    void testComputeCustomPortrait_NoMetrics_UseAI() throws Exception {
        // Arrange
        List<TemplateDimension> dims = new ArrayList<>();
        TemplateDimension dim = new TemplateDimension();
        dim.setName("创新能力");
        dim.setCode("INNOVATION");
        dim.setMetrics(null); // 无指标
        dims.add(dim);
        
        String dimensionsJson = objectMapper.writeValueAsString(dims);
        template.setDimensionsJson(dimensionsJson);
        template.setAiSuggested(true);
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(features);
        when(aiClient.evaluateDimension(anyString())).thenReturn("4.5");

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertNotNull(response);
        assertEquals(1, response.getScores().size());
        assertEquals(4.5, response.getScores().get(0), 0.01);
        verify(aiClient).evaluateDimension(contains("创新能力"));
    }

    @Test
    @DisplayName("computeCustomPortrait - AI评分异常时返回默认分")
    void testComputeCustomPortrait_AIEvaluationError() throws Exception {
        // Arrange
        List<TemplateDimension> dims = new ArrayList<>();
        TemplateDimension dim = new TemplateDimension();
        dim.setName("测试维度");
        dim.setMetrics(Collections.emptyList());
        dims.add(dim);
        
        String dimensionsJson = objectMapper.writeValueAsString(dims);
        template.setDimensionsJson(dimensionsJson);
        template.setAiSuggested(true);
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(features);
        when(aiClient.evaluateDimension(anyString())).thenReturn("invalid number");

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertNotNull(response);
        assertEquals(1, response.getScores().size());
        assertEquals(3.0, response.getScores().get(0), 0.01); // 默认3.0
    }

    @Test
    @DisplayName("computeCustomPortrait - 数据不足标记")
    void testComputeCustomPortrait_InsufficientData() throws Exception {
        // Arrange
        List<TemplateDimension> dims = new ArrayList<>();
        TemplateDimension dim = new TemplateDimension();
        dim.setName("测试维度");
        
        // 指标对应的数据不存在
        TemplateMetricMapping metric = new TemplateMetricMapping();
        metric.setMetricKey("non_existent_metric");
        metric.setWeight(1.0);
        dim.setMetrics(Collections.singletonList(metric));
        dims.add(dim);
        
        String dimensionsJson = objectMapper.writeValueAsString(dims);
        template.setDimensionsJson(dimensionsJson);
        template.setAiSuggested(true);
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(features);

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertNotNull(response);
        DimensionDetail detail = response.getDimensions().get(0);
        assertTrue(detail.isInsufficientData());
        assertEquals(0.0, detail.getScore(), 0.01);
    }

    // ==================== 指标归一化测试 ====================

    @Test
    @DisplayName("computeCustomPortrait - 各类指标归一化")
    void testComputeCustomPortrait_MetricNormalization() throws Exception {
        // Arrange
        List<TemplateDimension> dims = new ArrayList<>();
        TemplateDimension dim = new TemplateDimension();
        dim.setName("综合测试");
        
        List<TemplateMetricMapping> metrics = new ArrayList<>();
        
        // 测试分数类指标 (0-100)
        TemplateMetricMapping m1 = new TemplateMetricMapping();
        m1.setMetricKey("avg_course_score");
        m1.setWeight(0.3);
        metrics.add(m1);
        
        // 测试比率类指标 (0-1)
        TemplateMetricMapping m2 = new TemplateMetricMapping();
        m2.setMetricKey("assignment_pass_rate");
        m2.setWeight(0.3);
        metrics.add(m2);
        
        // 测试团队贡献度 (0-100)
        TemplateMetricMapping m3 = new TemplateMetricMapping();
        m3.setMetricKey("contribution_rate_avg");
        m3.setWeight(0.4);
        metrics.add(m3);
        
        dim.setMetrics(metrics);
        dims.add(dim);
        
        String dimensionsJson = objectMapper.writeValueAsString(dims);
        template.setDimensionsJson(dimensionsJson);
        template.setAiSuggested(true);
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(features);

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertNotNull(response);
        assertEquals(1, response.getScores().size());
        Double score = response.getScores().get(0);
        assertTrue(score >= 0 && score <= 5, "得分应在0-5范围: " + score);
    }

    // ==================== generateScoringStandards JSON解析测试 ====================

    @Test
    @DisplayName("generateScoringStandards - 解析带markdown代码块的AI响应")
    void testGenerateScoringStandards_ParseMarkdownJson() throws Exception {
        // Arrange
        List<TemplateDimension> dims = createDimensionsWithoutMetrics();
        String aiResponse = "这是一些说明文字\n```json\n" +
            "{\"metrics\":[{\"metricKey\":\"avg_score\",\"weight\":0.5},{\"metricKey\":\"pass_rate\",\"weight\":0.5}]," +
            "\"textAnalysisRules\":\"test\"," +
            "\"scoringLogic\":\"test logic\"}\n```\n更多说明";
        
        when(aiClient.generateScoringStandard(anyString())).thenReturn(aiResponse);

        // Act
        List<TemplateDimension> result = service.generateScoringStandards(dims, Collections.singletonList(features));

        // Assert
        assertNotNull(result);
        TemplateDimension dim = result.get(0);
        assertNotNull(dim.getMetrics());
        assertEquals(2, dim.getMetrics().size());
    }

    @Test
    @DisplayName("generateScoringStandards - 解析纯JSON响应")
    void testGenerateScoringStandards_ParsePureJson() throws Exception {
        // Arrange
        List<TemplateDimension> dims = createDimensionsWithoutMetrics();
        String aiResponse = "{\"metrics\":[{\"metricKey\":\"avg_score\",\"weight\":1.0}]}";
        
        when(aiClient.generateScoringStandard(anyString())).thenReturn(aiResponse);

        // Act
        List<TemplateDimension> result = service.generateScoringStandards(dims, Collections.singletonList(features));

        // Assert
        assertNotNull(result);
        // 纯JSON可能解析失败,返回空metrics也是正常的
        assertNotNull(result.get(0).getMetrics());
    }

    @Test
    @DisplayName("generateScoringStandards - AI返回无效JSON时metrics为空")
    void testGenerateScoringStandards_InvalidJson_EmptyMetrics() throws Exception {
        // Arrange
        List<TemplateDimension> dims = createDimensionsWithoutMetrics();
        when(aiClient.generateScoringStandard(anyString())).thenReturn("invalid json");

        // Act
        List<TemplateDimension> result = service.generateScoringStandards(dims, Collections.singletonList(features));

        // Assert
        assertNotNull(result);
        // 无效JSON解析失败后,metrics应该为空列表
        for (TemplateDimension dim : result) {
            assertNotNull(dim.getMetrics());
            // 解析失败时返回空列表是正常的
        }
    }

    // ==================== recommendDataMapping解析测试 ====================

    @Test
    @DisplayName("recommendDataMapping - 解析失败返回空列表")
    void testRecommendDataMapping_ParseError() throws Exception {
        // Arrange
        when(aiClient.recommendMapping(anyString())).thenReturn("not a json array");

        // Act
        List<String> result = service.recommendDataMapping("测试", "测试描述");

        // Assert
        assertNotNull(result);
        assertTrue(result.isEmpty());
    }

    @Test
    @DisplayName("recommendDataMapping - 解析嵌套JSON")
    void testRecommendDataMapping_NestedJson() throws Exception {
        // Arrange
        String aiResponse = "{\"recommended_fields\":[\"field1\",\"field2\"]}";
        when(aiClient.recommendMapping(anyString())).thenReturn(aiResponse);

        // Act
        List<String> result = service.recommendDataMapping("测试", "测试");

        // Assert
        assertNotNull(result);
        // 如果解析失败应返回空列表
    }

    // ==================== 边界值和异常测试 ====================

    @Test
    @DisplayName("computeCustomPortrait - 学生数据为null的指标")
    void testComputeCustomPortrait_NullMetricValues() throws Exception {
        // Arrange
        List<TemplateDimension> dims = createDimensionsWithMetrics();
        String dimensionsJson = objectMapper.writeValueAsString(dims);
        template.setDimensionsJson(dimensionsJson);
        template.setAiSuggested(true);
        
        StudentFeatureContext ctx = createFeatures();
        ctx.getNumericMetrics().clear(); // 清空所有指标
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(ctx);

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertNotNull(response);
        // 应该标记数据不足
        for (DimensionDetail detail : response.getDimensions()) {
            assertTrue(detail.isInsufficientData() || detail.getScore() == 0.0);
        }
    }

    @Test
    @DisplayName("computeCustomPortrait - 极端高分")
    void testComputeCustomPortrait_ExtremeHighScores() throws Exception {
        // Arrange
        List<TemplateDimension> dims = createDimensionsWithMetrics();
        String dimensionsJson = objectMapper.writeValueAsString(dims);
        template.setDimensionsJson(dimensionsJson);
        template.setAiSuggested(true);
        
        StudentFeatureContext ctx = createFeatures();
        // 设置所有指标为满分
        ctx.getNumericMetrics().put("avg_course_score", 100.0);
        ctx.getNumericMetrics().put("avg_assignment_score", 100.0);
        ctx.getNumericMetrics().put("assignment_pass_rate", 1.0);
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(ctx);

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertNotNull(response);
        for (Double score : response.getScores()) {
            assertTrue(score <= 5.0, "分数不应超过5.0: " + score);
        }
    }

    @Test
    @DisplayName("computeCustomPortrait - 极端低分")
    void testComputeCustomPortrait_ExtremeLowScores() throws Exception {
        // Arrange
        List<TemplateDimension> dims = createDimensionsWithMetrics();
        String dimensionsJson = objectMapper.writeValueAsString(dims);
        template.setDimensionsJson(dimensionsJson);
        template.setAiSuggested(true);
        
        StudentFeatureContext ctx = createFeatures();
        // 设置所有指标为最低分
        ctx.getNumericMetrics().put("avg_course_score", 0.0);
        ctx.getNumericMetrics().put("avg_assignment_score", 0.0);
        ctx.getNumericMetrics().put("assignment_pass_rate", 0.0);
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(ctx);

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertNotNull(response);
        for (Double score : response.getScores()) {
            assertTrue(score >= 0.0, "分数不应低于0.0: " + score);
        }
    }

    @Test
    @DisplayName("extractTeacherCommentKeywords - 过滤空评语")
    void testExtractTeacherCommentKeywords_FilterEmpty() {
        // Arrange
        StudentFeatureContext ctx = createFeatures();
        List<StudentFeatureContext.TeacherComment> comments = new ArrayList<>();
        
        StudentFeatureContext.TeacherComment empty1 = new StudentFeatureContext.TeacherComment();
        empty1.setCommentText("");
        comments.add(empty1);
        
        StudentFeatureContext.TeacherComment empty2 = new StudentFeatureContext.TeacherComment();
        empty2.setCommentText("   ");
        comments.add(empty2);
        
        StudentFeatureContext.TeacherComment valid = new StudentFeatureContext.TeacherComment();
        valid.setCommentText("有效评语");
        comments.add(valid);
        
        ctx.setTeacherComments(comments);
        
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(ctx);
        when(aiClient.extractKeywords(anyList())).thenReturn("{\"关键词\": 1}");

        // Act
        Map<String, Integer> result = service.extractTeacherCommentKeywords(STUDENT_ID);

        // Assert
        assertNotNull(result);
        // 应该只提取非空评语
        verify(aiClient).extractKeywords(argThat(list -> list.size() == 1));
    }

    @Test
    @DisplayName("computeCustomPortrait - 多种指标类型混合")
    void testComputeCustomPortrait_MixedMetricTypes() throws Exception {
        // Arrange
        List<TemplateDimension> dims = new ArrayList<>();
        TemplateDimension dim = new TemplateDimension();
        dim.setName("综合能力");
        
        List<TemplateMetricMapping> metrics = new ArrayList<>();
        
        // 学习速度指数 (特殊归一化)
        TemplateMetricMapping m1 = new TemplateMetricMapping();
        m1.setMetricKey("learning_speed_index");
        m1.setWeight(0.2);
        metrics.add(m1);
        
        // 作品点赞指数 (特殊归一化)
        TemplateMetricMapping m2 = new TemplateMetricMapping();
        m2.setMetricKey("submission_like_index");
        m2.setWeight(0.2);
        metrics.add(m2);
        
        // 讨论次数 (特殊归一化)
        TemplateMetricMapping m3 = new TemplateMetricMapping();
        m3.setMetricKey("discussion_count");
        m3.setWeight(0.2);
        metrics.add(m3);
        
        // 普通分数
        TemplateMetricMapping m4 = new TemplateMetricMapping();
        m4.setMetricKey("avg_course_score");
        m4.setWeight(0.4);
        metrics.add(m4);
        
        dim.setMetrics(metrics);
        dims.add(dim);
        
        String dimensionsJson = objectMapper.writeValueAsString(dims);
        template.setDimensionsJson(dimensionsJson);
        template.setAiSuggested(true);
        
        StudentFeatureContext ctx = createFeatures();
        ctx.getNumericMetrics().put("learning_speed_index", 1.2);
        ctx.getNumericMetrics().put("submission_like_index", 50.0);
        ctx.getNumericMetrics().put("discussion_count", 15.0);
        
        when(portraitTemplateMapper.selectById(TEMPLATE_ID)).thenReturn(template);
        when(studentFeatureExtractor.extractFeatures(STUDENT_ID)).thenReturn(ctx);

        // Act
        PortraitResponse response = service.computeCustomPortrait(STUDENT_ID, TEMPLATE_ID);

        // Assert
        assertNotNull(response);
        assertEquals(1, response.getScores().size());
        Double score = response.getScores().get(0);
        assertTrue(score >= 0 && score <= 5);
    }
}
